#import "NSManagedObjectContext+SMRAdditions.h"


NSString* const kSMRUniquePlistKey = @"SMRUniquePlistKey";


@interface NSManagedObjectContext (SMRPrivateAdditions)
-(NSManagedObject *)loadManagedObjectWithBaseEntityName:(NSString *)entityName objectDictionary:(NSDictionary *)objectDictionary;
-(BOOL)loadManagedObject_:(NSManagedObject *)object key:(NSString *)propertyKey value:(id)propertyValue;
-(NSManagedObject *)insertNewObjectForEntityForName_:(NSString *)entityName uniqueKey:(NSString *)uniqueKey uniqueValue:(id)uniqueValue;
@end


#pragma mark -
@implementation NSManagedObjectContext (SMRAdditions)

-(void)smrDeleteObjects:(NSArray *)objects {
    for (NSManagedObject *object in objects)
        [self deleteObject:object];
}

// ATTRIBUTION:
// This category is courtesy of Matt Gallagher
// http://cocoawithlove.com/2008/03/core-data-one-line-fetch.html
// Thanks, Matt

-(NSArray *)smrFetchObjectsForEntityName:(NSString *)newEntityName withPredicate:(id)stringOrPredicate, ... {
    NSEntityDescription *entity = [NSEntityDescription entityForName:newEntityName inManagedObjectContext:self];
    NSFetchRequest *request = [[[NSFetchRequest alloc] init] autorelease];
    [request setEntity:entity];
    
    if (stringOrPredicate) {
        NSPredicate *predicate;
        if ([stringOrPredicate isKindOfClass:[NSString class]]) {
            va_list variadicArguments;
            va_start(variadicArguments, stringOrPredicate);
            predicate = [NSPredicate predicateWithFormat:stringOrPredicate
                arguments:variadicArguments];
            va_end(variadicArguments);
        }
        else {
            NSAssert([stringOrPredicate isKindOfClass:[NSPredicate class]], @"Second parameter passed to %s is of unexpected class %@", sel_getName(_cmd), [stringOrPredicate className]);
            predicate = (NSPredicate *)stringOrPredicate;
        }
        [request setPredicate:predicate];
    }
     
    NSError *error = nil;
    NSArray *results = [self executeFetchRequest:request error:&error];
    if (error)
        [NSException raise:NSGenericException format:@"%@", [error description]];
    
    return results;
}

-(NSUInteger)smrCountFetchObjectForEntityName:(NSString *)newEntityName withPredicate:(id)stringOrPredicate, ... {
    NSEntityDescription *entity = [NSEntityDescription entityForName:newEntityName inManagedObjectContext:self];
    NSFetchRequest *request = [[[NSFetchRequest alloc] init] autorelease];
    [request setEntity:entity];
    
    if (stringOrPredicate) {
        NSPredicate *predicate;
        if ([stringOrPredicate isKindOfClass:[NSString class]]) {
            va_list variadicArguments;
            va_start(variadicArguments, stringOrPredicate);
            predicate = [NSPredicate predicateWithFormat:stringOrPredicate
											   arguments:variadicArguments];
            va_end(variadicArguments);
        }
        else {
            NSAssert([stringOrPredicate isKindOfClass:[NSPredicate class]], @"Second parameter passed to %s is of unexpected class %@", sel_getName(_cmd), [stringOrPredicate className]);
            predicate = (NSPredicate *)stringOrPredicate;
        }
        [request setPredicate:predicate];
    }
	
    NSError *error = nil;
	NSUInteger result = [self countForFetchRequest:request error:&error];
    if (error)
        [NSException raise:NSGenericException format:@"%@", [error description]];
    
    return result;
}

// This is a variation on Matt Galagher's category that returns the first result or nil
-(id)smrFetchFirstObjectForEntityName:(NSString *)newEntityName withPredicate:(id)stringOrPredicate, ... {
    NSEntityDescription *entity = [NSEntityDescription entityForName:newEntityName inManagedObjectContext:self];
    NSFetchRequest *request = [[[NSFetchRequest alloc] init] autorelease];
    [request setEntity:entity];
    
    if (stringOrPredicate) {
        NSPredicate *predicate;
        if ([stringOrPredicate isKindOfClass:[NSString class]]) {
            va_list variadicArguments;
            va_start(variadicArguments, stringOrPredicate);
            predicate = [NSPredicate predicateWithFormat:stringOrPredicate
											   arguments:variadicArguments];
            va_end(variadicArguments);
        }
        else {
            NSAssert([stringOrPredicate isKindOfClass:[NSPredicate class]], @"Second parameter passed to %s is of unexpected class %@", sel_getName(_cmd), [stringOrPredicate className]);
            predicate = (NSPredicate *)stringOrPredicate;
        }
        [request setPredicate:predicate];
    }
	
    NSError *error = nil;
    NSArray *results = [self executeFetchRequest:request error:&error];
    if (error)
        [NSException raise:NSGenericException format:@"%@", [error description]];
    
	if (!results || [results count] == 0)
		return nil;
	return [results objectAtIndex:0];
}

// This is a another variation on Matt Gallagher's category that includes sorting
- (NSArray *)smrFetchObjectsForEntityName:(NSString *)newEntityName sortedWithKey:(NSString *)sortKey ascending:(BOOL)ascending withPredicate:(id)stringOrPredicate, ... {
    NSEntityDescription *entity = [NSEntityDescription entityForName:newEntityName inManagedObjectContext:self];
    NSFetchRequest *request = [[[NSFetchRequest alloc] init] autorelease];
    [request setEntity:entity];
    
    if (stringOrPredicate) {
        NSPredicate *predicate;
        if ([stringOrPredicate isKindOfClass:[NSString class]]) {
            va_list variadicArguments;
            va_start(variadicArguments, stringOrPredicate);
            predicate = [NSPredicate predicateWithFormat:stringOrPredicate
                arguments:variadicArguments];
            va_end(variadicArguments);
        }
        else {
            NSAssert([stringOrPredicate isKindOfClass:[NSPredicate class]], @"Second parameter passed to %s is of unexpected class %@", sel_getName(_cmd), [stringOrPredicate className]);
            predicate = (NSPredicate *)stringOrPredicate;
        }
        [request setPredicate:predicate];
    }
     
    NSError *error = nil;
    NSArray *results = [self executeFetchRequest:request error:&error];
    if (error)
        [NSException raise:NSGenericException format:@"%@", [error description]];
    return [results sortedArrayUsingDescriptors:[NSArray arrayWithObject:[[NSSortDescriptor alloc] initWithKey:sortKey ascending:ascending]]];
}

- (NSArray *)smrFetchObjectsForEntityName:(NSString *)newEntityName sortedWithDescriptors:(NSArray *)sortDescriptorsArray withPredicate:(id)stringOrPredicate, ... {
    NSEntityDescription *entity = [NSEntityDescription entityForName:newEntityName inManagedObjectContext:self];
    NSFetchRequest *request = [[[NSFetchRequest alloc] init] autorelease];
    [request setEntity:entity];
    
    if (stringOrPredicate) {
        NSPredicate *predicate;
        if ([stringOrPredicate isKindOfClass:[NSString class]]) {
            va_list variadicArguments;
            va_start(variadicArguments, stringOrPredicate);
            predicate = [NSPredicate predicateWithFormat:stringOrPredicate
                arguments:variadicArguments];
            va_end(variadicArguments);
        }
        else {
            NSAssert([stringOrPredicate isKindOfClass:[NSPredicate class]], @"Second parameter passed to %s is of unexpected class %@", sel_getName(_cmd), [stringOrPredicate className]);
            predicate = (NSPredicate *)stringOrPredicate;
        }
        [request setPredicate:predicate];
    }
     
    NSError *error = nil;
    NSArray *results = [self executeFetchRequest:request error:&error];
    if (error)
        [NSException raise:NSGenericException format:@"%@",[error description]];
    return [results sortedArrayUsingDescriptors:sortDescriptorsArray];
}

// ATTRIBUTION:
// This category was modified to use the safer version from Matt Gallagher
// http://cocoawithlove.com/2008/08/safely-fetching-nsmanagedobject-by-uri.html
// Thanks, Matt

-(NSManagedObject *)smrObjectWithURI:(NSURL *)uri {
	NSManagedObjectID *objectID = [[self persistentStoreCoordinator] managedObjectIDForURIRepresentation:uri];
    if (!objectID)
        return nil;
    
    NSManagedObject *objectForID = [self objectWithID:objectID];
    if (![objectForID isFault])
        return objectForID;
	
    NSFetchRequest *request = [[[NSFetchRequest alloc] init] autorelease];
    [request setEntity:[objectID entity]];
    
    // Equivalent to predicate = [NSPredicate predicateWithFormat:@"SELF = %@", objectForID];
    NSPredicate *predicate = [NSComparisonPredicate predicateWithLeftExpression: [NSExpression expressionForEvaluatedObject] rightExpression:[NSExpression expressionForConstantValue:objectForID] modifier:NSDirectPredicateModifier type:NSEqualToPredicateOperatorType options:0];
    [request setPredicate:predicate];
	
    NSArray *results = [self executeFetchRequest:request error:nil];
    if ([results count] > 0 )
        return [results objectAtIndex:0];
    return nil;
}

-(NSManagedObject *)smrObjectWithURIString:(NSString *)uriString {
    NSURL *uri = [NSURL URLWithString:uriString];
    return [self smrObjectWithURI:uri];
}

-(NSArray *)smrObjectsWithURIs:(NSArray *)uriArray {
	NSMutableArray *objects = [NSMutableArray arrayWithCapacity:[uriArray count]];
	for (NSURL *uri in uriArray) {
		id object = [self smrObjectWithURI:uri];
		if (object)
			[objects addObject:object];
	}
	return objects;
}

-(NSArray *)smrObjectsWithURIStrings:(NSArray *)uriStringArray {
	NSMutableArray *uriArray = [NSMutableArray arrayWithCapacity:[uriStringArray count]];
	for (NSString *uriString in uriStringArray) {
		NSURL *uri = [NSURL URLWithString:uriString];
		if (uri)
			[uriArray addObject:uri];
	}
	return [self smrObjectsWithURIs:uriArray];
}

-(BOOL)smrLoadObjectsFromURL:(NSURL *)theURL baseEntityName:(NSString *)entityName {
	NSDictionary *plistDictionary = [NSDictionary dictionaryWithContentsOfURL:theURL];
    for (NSString *objectKey in plistDictionary) {
        id objectDictionary = [plistDictionary objectForKey:objectKey];
        if (![self loadManagedObjectWithBaseEntityName:entityName objectDictionary:objectDictionary])
            return NO;
    }
    return YES;
}

-(BOOL)loadManagedObject_:(NSManagedObject *)object key:(NSString *)propertyKey value:(id)propertyValue {
    NSEntityDescription *entityDescription = [object entity];
    NSDictionary *properties = [entityDescription propertiesByName];
    id propertyDescription = [properties objectForKey:propertyKey];
    if (propertyDescription == nil) {
        [NSException raise:NSInvalidArgumentException format:@"Property %@ not valid for entity %@", propertyKey, [entityDescription name]];
        return NO;
    }
    if ([propertyDescription isKindOfClass:[NSAttributeDescription class]]) {
        [object setValue:propertyValue forKey:propertyKey];
        return YES;
    }
    if (![propertyDescription isKindOfClass:[NSRelationshipDescription class]]) {
        LogInfo(@"Unable to load property %@ with property description type %@ for entity %@", propertyKey, [propertyDescription className], [entityDescription name]);
        return NO;
    }
    NSRelationshipDescription *relationshipDescription = (NSRelationshipDescription *)propertyDescription;
    BOOL toMany = [relationshipDescription isToMany];
    if ((!toMany && ![propertyValue isKindOfClass:[NSDictionary class]]) || (toMany && ![propertyValue isKindOfClass:[NSArray class]])) {
        LogInfo(@"Unable to load relationship %@ with object of type %@ for entity %@.  Expected NSDictionary (or NSArray of NSDictionary objects) to represent the target object in the relationship", propertyKey, [propertyValue className], [entityDescription name]);
        return NO;
    }
    
    NSString *destinationEntityName = [[relationshipDescription destinationEntity] name];
    if (!toMany) {
        NSManagedObject *toOneObject = [self loadManagedObjectWithBaseEntityName:destinationEntityName objectDictionary:propertyValue];
        if (toOneObject)
            [object setValue:toOneObject forKey:propertyKey];
        return toOneObject != nil;
    }
    
    NSMutableSet *mutablePropertySet = [object mutableSetValueForKey:propertyKey];
    for (NSDictionary *destinationObjectDictionary in propertyValue) {
        NSManagedObject *toManyObject = [self loadManagedObjectWithBaseEntityName:destinationEntityName objectDictionary:destinationObjectDictionary];
        if (!toManyObject)
            return NO;
        [mutablePropertySet addObject:toManyObject];
    }
    return YES;
}

-(NSManagedObject *)loadManagedObjectWithBaseEntityName:(NSString *)entityName objectDictionary:(NSDictionary *)objectDictionary {
    NSString *uniqueKey = [objectDictionary valueForKey:kSMRUniquePlistKey];
    NSManagedObject *baseObject = nil;
    if (!uniqueKey)
        baseObject = [NSEntityDescription insertNewObjectForEntityForName:entityName inManagedObjectContext:self];
    else {
        id uniqueValue = [objectDictionary valueForKey:uniqueKey];
        baseObject = [self insertNewObjectForEntityForName_:entityName uniqueKey:uniqueKey uniqueValue:uniqueValue];
    }
    
    for (NSString *propertyKey in objectDictionary) {
        if ([propertyKey isEqualToString:kSMRUniquePlistKey])
            continue;
        id propertyValue = [objectDictionary objectForKey:propertyKey];
        if (![self loadManagedObject_:baseObject key:propertyKey value:propertyValue])
            return nil;
    }
    return baseObject;
}

-(NSManagedObject *)insertNewObjectForEntityForName_:(NSString *)entityName uniqueKey:(NSString *)uniqueKey uniqueValue:(id)uniqueValue {
    NSManagedObject *resultObject = [self smrFetchFirstObjectForEntityName:entityName withPredicate:@"%K == %@", uniqueKey, uniqueValue];
    if (!resultObject)
        resultObject = [NSEntityDescription insertNewObjectForEntityForName:entityName inManagedObjectContext:self];
    return resultObject;
}

@end
